package org.xrebel.support;

import io.methvin.watcher.DirectoryChangeEvent;
import io.methvin.watcher.DirectoryWatcher;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.ReflectionUtils;
import org.xrebel.configure.XrebelProperties;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.stream.Collectors;

public final class MybatisXmlReload {

    private static final Logger logger = LoggerFactory.getLogger(MybatisXmlReload.class);

    final String mavenJavaDir = "/src/main/java";
    final String mavenResourcesDir = "/src/main/resources";
    final String classPathTargetDir = File.separator + "target" + File.separator + "classes";
    final PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();

    private final Executor executor;
    private DirectoryWatcher watcher;
    private final XrebelProperties xrebelProperties;
    private final List<SqlSessionFactory> sqlSessionFactories;

    public MybatisXmlReload(XrebelProperties xrebelProperties, List<SqlSessionFactory> sqlSessionFactories) {
        this.xrebelProperties = xrebelProperties;
        this.sqlSessionFactories = sqlSessionFactories;

        executor = Executors.newFixedThreadPool(1, (Runnable runnable) -> {
            final Thread thread = new Thread(runnable);

            thread.setName(xrebelProperties.getThreadNamePrefix());
            thread.setDaemon(true);
            return thread;
        });
    }

    /**
     * 创建监听指定mybatis xml文件的任务
     *
     * @throws IOException IO异常
     */
    public void createMybatisXmlReloadTask() throws IOException {
        // 创建指定文件变更监听处理任务
        startDirectoryWatcher(generateRawMapperLocations());
    }

    /**
     * 创建监听指定mybatis xml文件的任务
     *
     * @throws IOException IO异常
     */
    public void restartWatchTask(List<Resource> resources) throws IOException {
        // 关闭原有程序
        if (Objects.nonNull(watcher)) {
            watcher.close();
        }

        // 重新创建监控程序
        if (xrebelProperties.isEnabled()) {
            // 读取配置资源，并添加自定义资源
            final List<Resource> rawMapperLocations = generateRawMapperLocations();
            rawMapperLocations.addAll(resources);

            startDirectoryWatcher(rawMapperLocations);
        }
    }

    /**
     * 根据配置文件生成原始需要监控的的资源
     *
     * @return 资源
     */
    private List<Resource> generateRawMapperLocations() {
        return xrebelProperties.getMapperLocations().stream()
                .flatMap((String location) -> getResources(patternResolver, location).stream())
                .collect(Collectors.toList());
    }

    /**
     * 根据给定的路径和资源创建监控程序
     *
     * @param rawMapperLocations 监控资源
     */
    private void startDirectoryWatcher(final List<Resource> rawMapperLocations) throws IOException {
        // 判断是否需要创建相应任务
        if (Objects.nonNull(rawMapperLocations) && !rawMapperLocations.isEmpty()) {
            final List<Path> monitorPaths = new ArrayList<>();
            final List<Resource> mapperLocations = new ArrayList<>(rawMapperLocations.size() << 3);

            // 判断xml文件存放位置，只读取文件类型的xml文件，jar里的xml文件不做读取
            for (Resource mapperLocation : rawMapperLocations) {
                if (mapperLocation.isFile()) {
                    final String absolutePath = mapperLocation.getFile().getAbsolutePath();

                    // 先从 maven_resources_dir目录下找xml文件目录，不存在就去 maven_java_dir 下找xml文件目录，都找不到就只能取target目录下xml文件目录
                    File tmpFile = new File(absolutePath.replace(classPathTargetDir, mavenResourcesDir));
                    if (!tmpFile.exists()) {
                        tmpFile = new File(absolutePath.replace(classPathTargetDir, mavenJavaDir));
                    }
                    if (tmpFile.exists()) {
                        mapperLocations.add(new FileSystemResource(tmpFile));
                        monitorPaths.add(Paths.get(tmpFile.getParent()));
                    } else {
                        monitorPaths.add(Paths.get(mapperLocation.getFile().getParent()));
                    }
                }
            }

            // 创建指定文件变更监听处理任务
            watcher = DirectoryWatcher.builder().paths(monitorPaths).listener((DirectoryChangeEvent changeEvent) -> {
                logger.info("开始检查是否重新加载mybatis.xml文件...");

                switch (changeEvent.eventType()) {
                    case MODIFY: /* file modified */
                    case OVERFLOW: /* file overflow */
                        final Path modifyPath = changeEvent.path();
                        final String absolutePath = modifyPath.toFile().getAbsolutePath();
                        if (!new File(absolutePath).exists()) {
                            break;
                        }

                        logger.info("mybatis xml file has changed: '{}'", absolutePath);

                        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories) {
                            try {
                                final Configuration configuration = sqlSessionFactory.getConfiguration();
                                Class<?> tClass = configuration.getClass(), aClass = configuration.getClass();
                                if (tClass.getSimpleName().equals("MybatisConfiguration")) {
                                    aClass = Configuration.class;
                                }
                                final Set<String> loadedResources = (Set<String>) getFieldValue(configuration, aClass, "loadedResources");
                                loadedResources.clear();

                                // mybatis3.5.6中下面的属性在Configuration类中，直接从Configuration中无法获取
                                // 优先从MybatisConfiguration读取配置，若找不到则在父类Configuration中获取
                                Map<String, ResultMap> resultMaps = null;
                                try {
                                    resultMaps = (Map<String, ResultMap>) getFieldValue(configuration, tClass, "resultMaps");
                                } catch (NoSuchFieldException exception) {
                                    resultMaps = (Map<String, ResultMap>) getFieldValue(configuration, aClass, "resultMaps");
                                }

                                Map<String, XNode> sqlFragmentsMaps = null;
                                try {
                                    sqlFragmentsMaps = (Map<String, XNode>) getFieldValue(configuration, tClass, "sqlFragments");
                                } catch (NoSuchFieldException ex) {
                                    sqlFragmentsMaps = (Map<String, XNode>) getFieldValue(configuration, aClass, "sqlFragments");
                                }

                                Map<String, MappedStatement> mappedStatementMaps = null;
                                try {
                                    mappedStatementMaps = (Map<String, MappedStatement>) getFieldValue(configuration, tClass, "mappedStatements");
                                } catch (NoSuchFieldException ex) {
                                    mappedStatementMaps = (Map<String, MappedStatement>) getFieldValue(configuration, aClass, "mappedStatements");
                                }

                                for (Resource mapperLocation : mapperLocations) {
                                    if (!mapperLocation.isFile() || !absolutePath.equals(mapperLocation.getFile().getAbsolutePath())) {
                                        continue;
                                    }

                                    final XPathParser parser;
                                    try (final InputStream inputStream = mapperLocation.getInputStream()) {
                                        parser = new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver());
                                    } catch (Exception ignored) {
                                        logger.error("读取文件 {} 错误", absolutePath);
                                        continue;
                                    }

                                    final XNode mapperXnode = parser.evalNode("/mapper");
                                    final String namespace = mapperXnode.getStringAttribute("namespace");
                                    final List<XNode> resultMapNodes = mapperXnode.evalNodes("/mapper/resultMap");

                                    for (XNode xNode : resultMapNodes) {
                                        final String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
                                        final Iterator<Map.Entry<String, ResultMap>> iterator = resultMaps.entrySet().iterator();

                                        while (iterator.hasNext()) {
                                            final String key = iterator.next().getKey();

                                            if (key.contains(namespace + "." + id)) {
                                                iterator.remove();
                                            }
                                            // 处理 association、collection标签
                                            if (key.contains(namespace + ".mapper_resultMap")) {
                                                iterator.remove();
                                            }
                                        }
                                    }

                                    for (XNode sqlNode : mapperXnode.evalNodes("/mapper/sql")) {
                                        final String id = sqlNode.getStringAttribute("id", sqlNode.getValueBasedIdentifier());
                                        sqlFragmentsMaps.remove(namespace + "." + id);
                                    }

                                    for (XNode xNode : mapperXnode.evalNodes("select|insert|update|delete")) {
                                        final String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
                                        mappedStatementMaps.remove(namespace + "." + id);
                                    }

                                    // 重新加载指定文件
                                    try (final InputStream inputStream = mapperLocation.getInputStream()) {
                                        new XMLMapperBuilder(inputStream, configuration, mapperLocation.toString(), configuration.getSqlFragments()).parse();
                                    } catch (Exception exception) {
                                        logger.error(exception.getMessage(), exception);
                                    }
                                    logger.info("mapperLocation reload success: '{}'", mapperLocation);
                                }
                            } catch (Exception exception) {
                                logger.error(exception.getMessage(), exception);
                            }
                        }
                        break;
                    case CREATE: /* file created */
                    case DELETE: /* file deleted */
                        break;
                }
            }).build();

            // 启动监控任务
            watcher.watchAsync(executor);
        }
    }

    /**
     * 根据xml路径获取对应实际文件
     *
     * @param location 文件位置
     * @return Resource[]
     */
    private List<Resource> getResources(PathMatchingResourcePatternResolver patternResolver, String location) {
        try {
            return Arrays.stream(patternResolver.getResources(location))
                    .peek(resource -> logger.info("加载到 mybatis xml 文件: {}", resource))
                    .collect(Collectors.toList());
        } catch (IOException exception) {
            // 记录详细的异常信息
            ReflectionUtils.rethrowRuntimeException(exception);
            return Collections.emptyList();
        }
    }

    /**
     * 根据反射获取Configuration对象中属性
     *
     * @param targetConfiguration 源对象
     * @param aClass              兑现隔离性
     * @param filed               指定的字段
     * @return 指定字段值
     * @throws NoSuchFieldException   字段不存在
     */
    private static Object getFieldValue(Configuration targetConfiguration, Class<?> aClass, String filed) throws
            NoSuchFieldException, IllegalAccessException {
        final Field resultMapsField = aClass.getDeclaredField(filed);
        resultMapsField.setAccessible(true);

        return resultMapsField.get(targetConfiguration);
    }
}
